package com.bj58.argo.internal;

import com.bj58.argo.ActionResult;
import com.bj58.argo.Argo;
import com.bj58.argo.BeatContext;
import com.bj58.argo.inject.ArgoSystem;
import com.bj58.argo.internal.actionresult.StatusCodeActionResult;
import com.bj58.argo.logs.Logger;
import com.bj58.argo.route.Router;
import com.bj58.argo.servlet.ArgoDispatcher;
import com.bj58.argo.servlet.ArgoRequest;
import com.bj58.argo.utils.OnlyOnceCondition;
import com.google.common.io.Closeables;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;

import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.MultipartConfigElement;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 *
 * 用于处理Rest请求调度的核心类
 *
 */
@Singleton
public class DefaultArgoDispatcher implements ArgoDispatcher {

    private final Argo argo;

    private final Router router;

    private final StatusCodeActionResult statusCodeActionResult;

    private final Key<BeatContext> defaultBeatContextKey = Key.get(BeatContext.class, ArgoSystem.class);

    private final MultipartConfigElement config;

    private final Logger logger;


    @Inject
    public DefaultArgoDispatcher(Argo argo, Router router, StatusCodeActionResult statusCodeActionResult, MultipartConfigElement config) {

        this.argo = argo;

        this.router = router;
        this.statusCodeActionResult = statusCodeActionResult;
        this.config = config;

        this.logger = argo.getLogger(this.getClass());

        logger.info("constructed.", this.getClass());
    }

    @Override
    public void init() {
    }

    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) {

        ArgoRequest argoRequest = new ArgoRequest(request, config);

        try {
            BeatContext beatContext = bindBeatContext(argoRequest, response);

            route(beatContext);
        } finally {
            Closeables.closeQuietly(argoRequest);
        }
    }

    private BeatContext bindBeatContext(HttpServletRequest request, HttpServletResponse response) {
        Context context = new Context(request, response);
        localContext.set(context);

        BeatContext beat = argo.injector().getInstance(defaultBeatContextKey);
        // 增加默认参数到model
        beat.getModel().add("__beat", beat);
        context.setBeat(beat);
        return beat;
    }

    private void route(BeatContext beat) {
        try {
            ActionResult result = router.route(beat);

            if (ActionResult.NULL == result)
                result = statusCodeActionResult.getSc404();

            result.render(beat);

        } catch (Exception e) {

            statusCodeActionResult.render405(beat);

            e.printStackTrace();

            logger.error(String.format("fail to route. url:%s", beat.getClient().getRelativeUrl()), e);

            //TODO: catch any exceptions.

        } finally {
            localContext.remove();
        }
    }

    public void destroy() {
    }

    public HttpServletRequest currentRequest() {
        return getContext().getRequest();
    }

    public HttpServletResponse currentResponse() {
        return getContext().getResponse();
    }

    public BeatContext currentBeatContext() {
        return getContext().getBeat();
    }


    private Context getContext() {
        Context context = localContext.get();
        if (context == null) {
            throw new OutOfScopeException("Cannot access scoped object. Either we"
                    + " are not currently inside an HTTP Servlet currentRequest, or you may"
                    + " have forgotten to apply " + DefaultArgoDispatcher.class.getName()
                    + " as a servlet filter for this currentRequest.");
        }
        return context;
    }

    final ThreadLocal<Context> localContext = new ThreadLocal<Context>();

    private static class Context {

        final HttpServletRequest request;
        final HttpServletResponse response;

        BeatContext beat;

        OnlyOnceCondition onlyOnce = OnlyOnceCondition.create("The current beat has been created.");

        Context(HttpServletRequest request, HttpServletResponse response) {
            this.request = request;
            this.response = response;
        }

        HttpServletRequest getRequest() {
            return request;
        }

        HttpServletResponse getResponse() {
            return response;
        }

        BeatContext getBeat() {
            return beat;
        }

        void setBeat(BeatContext beat) {
            onlyOnce.check();
            this.beat = beat;
        }
    }

}
